﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: DynamicPropertiesCollection.cs
//
//  Description: A list of dynamic properties.
// 
//--------------------------------------------------------------------------

using System;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System.Text; 

namespace Chili.Opf3.DynamicProperties
{
	/// <summary>
	/// Contains <see cref="Opf3.DynamicProperties.DynamicProperty">DynamicProperties</see> that 
	/// are used to save semi-structured data in the storage.
	/// </summary>
	/// <remarks>
	/// <para>
	/// This class contains a set of 
	/// <see cref="Opf3.DynamicProperties.DynamicProperty">DynamicProperties</see> allowing
	/// to extend dynamically an entity in the storage. The DynamicPropertiesContainer is set as type
	/// of property in the persistent object and that property is then mapped (using the 
	/// <see cref="Opf3.FieldAttribute">FieldAttribute</see>) to a field in the storage that can contain
	/// xml strings. 
	/// </para>
	/// <para>
	/// <see cref="Opf3.DynamicProperties.DynamicProperty">DynamicProperties</see> are saved
	/// to that field as xml string and restored after load. This feature allows to extend the entity
	/// dynamically. The user of the final program can theirfore add new fields to extend the functionality 
	/// of the program.
	/// </para>
	/// </remarks>
	/// <example>
	/// The following example shows how to use the DynamicPropertiesContainer in your application.
	/// <code lang="CS">
	/// [Persistent("USER")]
	/// public class User
	/// {
	///		private DynamicPropertiesCollection _dyn = 
	///			new DynamicPropertiesCollection();
	/// 
	///		[Field("DYN_PROPS")]
	///		public DynamicPropertiesCollection DynProps
	///		{
	///			get { return _dynProps; }
	///			set { _dynProps = value; }
	///		}
	/// 
	///		// ... Other properties and code
	/// }
	/// 
	/// // ... Other code
	/// 
	/// // Create a new User object.
	/// User user = new User();
	/// 
	/// // Create the DynamicProperty and set a value.
	/// DynamicProperty prop = new DynamicProperty("PLZ", typeof(string));
	/// prop.Value = "1234";
	/// 
	/// // Add the DynamicProperty to the container.
	/// user.DynProps.Add(prop);
	/// 
	/// // ... Other code
	/// 
	/// // Persist the object.
	/// context.PersistChanges(user);
	/// 
	/// // Get the DynamicProperty with the name "PLZ"
	/// // and get the value as string.
	/// PLZLabel.Text = user.DynProps["PLZ"].ToString();
	/// </code>
	/// </example>
	[Serializable]
	public sealed class DynamicPropertiesCollection : IList<DynamicProperty>, IEnumerable, IUserDefinedType, ICloneable
	{
		private const string DYNAMIC_ELEMENTS_NODE = "opf3";
		private const string DYNAMIC_PROPERTIES_NODE = "dynamicProperties";
		private const string DYNAMIC_PROPERTY_NODE = "dynamicProperty";
		private const string DYNAMIC_PROPERTY_TYPE_ATTRIBUTE = "type";
		private const string DYNAMIC_TYPES_NODE = "dynamicTypes";
		private const string DYNAMIC_TYPE_NODE = "dynamicType";
		private const string ID_NODE = "id";
		private const string NAME_NODE = "name";
		private const string VALUES_NODE = "values";
		private const string VALUE_NODE = "value";

		private string _xml;
		private IDictionary<string, DynamicType> _types = new Dictionary<string, DynamicType>();
		private IList<DynamicProperty> _properties = new List<DynamicProperty>();

		/// <summary>
		/// Creates a new instance of the <see cref="DynamicPropertiesCollection">DynamicPropertiesCollection Class</see>.
		/// </summary>
		public DynamicPropertiesCollection()
		{
		}

		#region IUserDefinedType Members

		/// <summary>
		/// Populates the type that implements the interface (a type that can't be directly saved or 
		/// loaded from the storage) with data from the storage.
		/// </summary>
		/// <param name="value">The simple type representation in the storage (for example an xml string).</param>
		void IUserDefinedType.FromSimpleDataType(object value)
		{
			_xml = (string)value;
		}

		/// <summary>
		/// Converts the user-defined type to a type that can be persisted to the storage. You could
		/// return here for example an xml string or something else that can be saved to the storage.
		/// </summary>
		/// <returns>
		/// The object converted to a simple type that is saved to the storage.
		/// </returns>
		object IUserDefinedType.ToSimpleDataType()
		{
			return CreateXml();
		}

		/// <summary>
		/// Returns the type supported by the class implementing this interface. The supported type is the type
		/// that is returned when converting to a simple type and the type that is set when converting from the simple type.
		/// </summary>
		Type IUserDefinedType.SupportedType
		{
			get { return typeof(string); }
		}

		#endregion

		#region ICloneable Members

		/// <summary>
		/// Creates a new object that is a copy of the current instance.
		/// </summary>
		/// <returns>
		/// A new object that is a copy of this instance.
		/// </returns>
		public object Clone()
		{
			// Create a new instance of the DynamicPropertiesContainer.
			DynamicPropertiesCollection container = new DynamicPropertiesCollection();

			// Loop over all DynamicProperties. Clone them and add them to the container.
			foreach (DynamicProperty property in _properties)
				container.Add((DynamicProperty)property.Clone());
			foreach (KeyValuePair<string, DynamicType> type in _types)
				container._types.Add(type.Key, (DynamicType)type.Value.Clone());

			return container;
		}

		#endregion

		#region IList Members

		/// <summary>
		/// Returns the zero-based index of the first occurrence of a value in the container
		/// or in a portion of it.
		/// </summary>
		/// <param name="item">Item that's index in the container is returned.</param>
		public int IndexOf(DynamicProperty item)
		{
			ParseXml();
			return _properties.IndexOf(item);
		}

		/// <summary>
		/// Inserts an element into the container at the specified index.
		/// </summary>
		/// <param name="index">Index where the item is inserted.</param>
		/// <param name="item">Item that is inserted.</param>
		/// <exception cref="ArgumentOutOfRangeException">Index is not a valid index in the 
		/// <see cref="DynamicPropertiesCollection">DynamicPropertiesCollection</see>.</exception>
		public void Insert(int index, DynamicProperty item)
		{
			ParseXml();
			_properties.Insert(index, item);
	}

		/// <summary>
		/// Removes the item at the specified index.
		/// </summary>
		/// <param name="index">Index of the item that is removed.</param>
		/// <exception cref="ArgumentOutOfRangeException">Index is not a valid index in the 
		/// <see cref="DynamicPropertiesCollection">DynamicPropertiesCollection</see>.</exception>
		public void RemoveAt(int index)
		{
			ParseXml();

			// Get the dynamic property.
			DynamicProperty item = this[index];
			_properties.RemoveAt(index);
			// Check if to remove the dynamic type.
			CheckDynamicType(item);
		}

		/// <summary>
		/// Gets or sets the element at the specified index.
		/// </summary>
		/// <param name="index">Index of the item that is modified.</param>
		/// <exception cref="ArgumentOutOfRangeException">Index is not a valid index in the 
		/// <see cref="DynamicPropertiesCollection">DynamicPropertiesCollection</see>.</exception>
		public DynamicProperty this[int index]
		{
			get
			{
				ParseXml();
				return _properties[index];
			}
			set
			{
				ParseXml();
				DynamicProperty item = _properties[index];
				_properties[index] = value;

				// Check if to remove the dynamic type associated with the dynamic property.
				if (value != item)
					CheckDynamicType(item);
			}
		}

		#endregion

		#region ICollection Members

		/// <summary>
		/// Adds an item to the container.
		/// </summary>
		/// <param name="item">Item that is added to the container.</param>
		public void Add(DynamicProperty item)
		{
			ParseXml();
			_properties.Add(item);

			// Add the dynamic type to the dynamic types of the container. If available.
			if (item.DynamicType != null && !_types.ContainsKey(item.DynamicType.Name))
			{
				_types.Add(item.DynamicType.Name, item.DynamicType);
			}
		}

		/// <summary>
		/// Removes all items from the container.
		/// </summary>
		public void Clear()
		{
			_xml = null;
			_properties.Clear();
			_types.Clear();
		}

		/// <summary>
		/// Determines whether the container contains a specific value.
		/// </summary>
		/// <param name="item">Item that is searched for.</param>
		public bool Contains(DynamicProperty item)
		{
			ParseXml();
			return _properties.Contains(item);
		}

		/// <summary>
		/// Copies the elements of the container to an array, starting at a particular index.
		/// </summary>
		/// <param name="array">Array that is populated with data from the container.</param>
		/// <param name="arrayIndex">Index where to start.</param>
		/// <exception cref="System.ArgumentNullException">Array is null.</exception>
		/// <exception cref="System.ArgumentException">Array is multidimensional, array does not have zero-based
		/// indexing, the number of elements in the source is greater than the number of elements that the 
		/// destination array can contain or type T cannot be cast automatically to the type of the destination
		/// array.</exception>
		public void CopyTo(DynamicProperty[] array, int arrayIndex)
		{
			ParseXml();
			_properties.CopyTo(array, arrayIndex);
		}

		/// <summary>
		/// Gets the number of elements contained in the container.
		/// </summary>
		[XmlIgnore]
		public int Count
		{
			get 
			{
				ParseXml();
				return _properties.Count;
			}
		}

		/// <summary>
		/// Gets a value indicating whether the container is read-only.
		/// </summary>
		[XmlIgnore]
		public bool IsReadOnly
		{
			get { return false; }
		}

		/// <summary>
		/// Removes the first occurrence of a specific object from the container.
		/// </summary>
		/// <param name="item">Item that's first occurence is removed.</param>
		public bool Remove(DynamicProperty item)
		{
			ParseXml();
			bool removed = _properties.Remove(item);

			// Check if to remove the dynamic type.
			if (removed)
				CheckDynamicType(item);

			return removed;
		}

		#endregion

		#region IEnumerable<T> Members

		/// <summary>
		/// Returns an enumerator that iterates through the container.
		/// </summary>
		public IEnumerator<DynamicProperty> GetEnumerator()
		{
			ParseXml();
			return _properties.GetEnumerator();
		}

		#endregion

		#region IEnumerable Members

		/// <summary>
		/// Returns an enumerator that iterates through the container.
		/// </summary>
		IEnumerator IEnumerable.GetEnumerator()
		{
			ParseXml();
			return (IEnumerator)_properties.GetEnumerator();
		}

		#endregion

		/// <summary>
		/// Sets the value of element with the specified Identifier. If the elements dosent't exist create it
		/// </summary>
		/// <param name="id">Identifier of the <see cref="DynamicProperty">DynamicProperty</see>.</param>
		/// <param name="type">The type of the <see cref="DynamicProperty">DynamicProperty</see>.</param>
		/// <param name="value">Value of the <see cref="DynamicProperty">DynamicProperty</see>.</param>
		public DynamicProperty AddOrSet(Type type, string id, object value)
		{
			DynamicProperty property = this[id];

			if (property == null)
			{
				property = new DynamicProperty(type, id, value);
				this.Add(property);
			}
			else
				property.Value = value;

			return property;
		}

		/// <summary>
		/// Gets or sets the element with the specified Identifier.
		/// </summary>
		/// <param name="id">Identifier of the item that is modified.</param>
		/// <value>The <see cref="DynamicProperty">DynamicProperty</see> with the given id.</value>
		public DynamicProperty this[string id]
		{
			get
			{
				ParseXml();
				foreach (DynamicProperty item in _properties)
				{
					if (item.Id == id)
						return (DynamicProperty)item;
				}
				return null;
			}
			set
			{
				ParseXml();
				foreach (DynamicProperty item in _properties)
				{
					if (item.Id == id)
					{
						this.Remove(item);
						// Check if to remove the dynamic type.
						CheckDynamicType(item);
					}
				}
				this.Add(value);
			}
		}

		/// <summary>
		/// Checks if the <see cref="DynamicType">DynamicType</see> associated with the 
		/// <see cref="DynamicProperty">DynamicProperty</see> should be removed.
		/// </summary>
		/// <param name="item">The <see cref="DynamicProperty">DynamicProperty</see> that has been removed.</param>
		private void CheckDynamicType(DynamicProperty item)
		{
			// Remove also the dynamic type if no other dynamic property holds a reference to it.
			if (item.DynamicType != null)
			{
				foreach (DynamicProperty property in _properties)
				{
					if (property.DynamicType != null && property.DynamicType.Name == item.DynamicType.Name)
						return;
				}

				_types.Remove(item.DynamicType.Name);
			}
		}

		#region ParseXml

		/// <summary>
		/// Parses the xml that comes from the storage and creates DynamicProperties.
		/// </summary>
		private void ParseXml()
		{
			if (_xml == null || _xml.Length == 0)
				return;

			// Clear the dynamic properties.
			_properties.Clear();

			// Create the reader.
			using (XmlTextReader reader = new XmlTextReader(_xml, XmlNodeType.Document, null))
			{
				while (reader.Read())
				{
					switch(reader.Name)
					{
						// We found a dynamic property.
						case DYNAMIC_PROPERTY_NODE:
							ParseDynamicProperty(reader);
							break;
						case DYNAMIC_TYPE_NODE:
							ParseDynamicType(reader);
							break;
					}
				}
			}

			// Set the xml string to null. Avoids multiple parsing.
			_xml = null;
		}

		/// <summary>
		/// Parses a <see cref="DynamicProperty">DynamicProperty</see>.
		/// </summary>
		/// <param name="reader">The xml reader used to parse the <see cref="DynamicProperty">DynamicProperty</see>.</param>
		private void ParseDynamicProperty(XmlTextReader reader)
		{
			string id = null;
			object value = null;
			DynamicType _dynamicType = null;

			string t = reader.GetAttribute("type");
			// Check if the string contains the old class type (compatibility).
			if (t.Contains("Opf3.DynamicProperties"))
				t = t.Substring(t.IndexOf("[") + 1, t.IndexOf("]") - t.IndexOf("[") - 1);

			string dyn = reader.GetAttribute(DYNAMIC_TYPE_NODE);
			if (dyn != null)
				_dynamicType = _types[dyn];

			// get the type.
			Type type = Type.GetType(t, true);

			// loop over and check what to do.
			while (reader.Read() && reader.Name != DYNAMIC_PROPERTY_NODE)
			{
				// read the ID.
				if (reader.Name == ID_NODE)
					id = reader.ReadString();
				// read the value.
				else if (reader.Name == VALUE_NODE)
				{
					// check if we have to process an enum or if we can just change the string into the
					// appropriate type.
					if (type.IsEnum)
						value = Enum.Parse(type, reader.ReadString());
					else
						value = Convert.ChangeType(reader.ReadString(), type);
				}
				else if (reader.Name == DYNAMIC_TYPE_NODE && reader.IsStartElement())
					_dynamicType = ParseDynamicType(reader);
			}

			// create the dynamic property if we got all the required data.
			if (id != null)
			{
				DynamicProperty property = null;
				if (_dynamicType == null)
					property = new DynamicProperty(type, id);
				else
					property = new DynamicProperty(_dynamicType, id);
				property.Value = value;

				_properties.Add(property);
			}
		}

		/// <summary>
		/// Parses a <see cref="DynamicType">DynamicType</see>.
		/// </summary>
		/// <param name="reader">The xml reader used to parse the <see cref="DynamicType">DynamicType</see>.</param>
		private DynamicType ParseDynamicType(XmlTextReader reader)
		{
			DynamicType type = null;

			while (reader.Read() && reader.Name != DYNAMIC_TYPE_NODE && reader.IsStartElement())
			{
				if (reader.Name == NAME_NODE)
					type = new DynamicType(reader.ReadString());
				else if (reader.Name == VALUES_NODE)
					ParseValues(reader, type);
			}

			if (type != null && !_types.ContainsKey(type.Name))
				_types.Add(type.Name, type);

			return type;
		}

		/// <summary>
		/// Parses the values of the <see cref="DynamicType">DynamicType</see>.
		/// </summary>
		/// <param name="reader">The xml reader used to get the data from the xml document.</param>
		/// <param name="type">The <see cref="DynamicType">DynamicType</see> filled with the values.</param>
		private void ParseValues(XmlTextReader reader, DynamicType type)
		{
			while (reader.Read() && reader.Name != VALUES_NODE)
			{
				if (reader.Name == VALUE_NODE)
					type.Values.Add(reader.ReadString());
			}
		}

		#endregion

		#region CreateXml

		/// <summary>
		/// Creates the xml from the DynamicProperties managed by this container.
		/// </summary>
		private string CreateXml()
		{
			if (_xml != null)
				return _xml;
			// If no DynamicProperties have been found return null.
			if (_properties.Count == 0)
				return null;

			// to buid xml
			MemoryStream stream = new MemoryStream();

			XmlTextWriter textWriter = new XmlTextWriter(stream, new UTF8Encoding(false));
			textWriter.Formatting = Formatting.Indented;
			textWriter.WriteStartDocument();

			textWriter.WriteStartElement(DYNAMIC_ELEMENTS_NODE);
			textWriter.WriteStartElement(DYNAMIC_TYPES_NODE);
			// Loop over all DynamicTypes, get the xml and add it.
			foreach (KeyValuePair<string, DynamicType> type in _types)
			{
				// Add the dynamic type to the xml.
				CreateDynamicTypeXml(textWriter, type.Value);
			}
			textWriter.WriteEndElement();  // _dynamicTypesNode

			textWriter.WriteStartElement(DYNAMIC_PROPERTIES_NODE);
			// Loop over all DynamicProperties, get the xml and add it.
			foreach (DynamicProperty property in _properties)
			{
				// Don't add properties that are null.
				if (property != null)
				{
					CreateDynamicPropertyXml(textWriter, property);
				}
			}
			textWriter.WriteEndElement();  // _dynamicPropertiesNode
			textWriter.WriteEndElement(); // _dynamicElementsNode
			
			textWriter.Flush();
			return Encoding.UTF8.GetString(stream.GetBuffer()).TrimEnd('\0');
		}

		/// <summary>
		/// Creates the xml for a <see cref="DynamicType">DynamicType</see>.
		/// </summary>
		/// <param name="textWriter">The xml text writer used to create the xml.</param>
		/// <param name="type">The <see cref="DynamicType">DynamicType</see> transformed to xml.</param>
		private void CreateDynamicTypeXml(XmlTextWriter textWriter, DynamicType type)
		{
			textWriter.WriteStartElement(DYNAMIC_TYPE_NODE);
			textWriter.WriteStartElement(NAME_NODE);

			// Check whether the name of the dynamic type is null.
			if (type.Name == null)
				throw new DynamicPropertyException("Name of a dynamic type can't be null.");

			textWriter.WriteString(type.Name);
			textWriter.WriteEndElement();  // _nameNode

			textWriter.WriteStartElement(VALUES_NODE);

			// Check if the values of the dynamic types are not null.
			if (type.Values == null)
				throw new DynamicPropertyException("The values of a dynamic type can't be null.");

			foreach (string value in type.Values)
			{
				textWriter.WriteStartElement(VALUE_NODE);

				// Check if the value of the dynamic types are not null.
				if (value == null)
					throw new DynamicPropertyException("A value of a dynamic type can't be null.");

				textWriter.WriteString(value);
				textWriter.WriteEndElement();  //_valueNode
			}
			textWriter.WriteEndElement(); // _valuesNode
			textWriter.WriteEndElement(); // _dynamicTypeNode
		}

		/// <summary>
		/// Creates the xml for a <see cref="DynamicProperty">DynamicProperty</see>.
		/// </summary>
		/// <param name="textWriter">The xml text writer used to create the xml.</param>
		/// <param name="property">The <see cref="DynamicProperty">DynamicProperty</see> transformed to xml</param>
		private void CreateDynamicPropertyXml(XmlTextWriter textWriter, DynamicProperty property)
		{
			textWriter.WriteStartElement(DYNAMIC_PROPERTY_NODE);
			textWriter.WriteAttributeString(DYNAMIC_PROPERTY_TYPE_ATTRIBUTE, property.TypeAsString);

			if (property.DynamicType != null)
			{
				textWriter.WriteAttributeString(DYNAMIC_TYPE_NODE, property.DynamicType.Name);

				// Check if the value set for the type is valid.
				if (property.Value != typeof(string) || !property.DynamicType.Values.Contains((string)property.Value))
					throw new DynamicPropertyException("Dynamic type does not contain value!");
			}

			textWriter.WriteStartElement(ID_NODE);

			if (property.Id == null)
				throw new DynamicPropertyException("Id of a dynamic property can't be null.");

			textWriter.WriteString(property.Id);
			textWriter.WriteEndElement(); // _idNode

			textWriter.WriteStartElement(VALUE_NODE);
			if (property.Value != null)
			{
				// Check if the value of the dynamic property is the same type as the type of the dynamic property.
				if (property.Value.GetType() != property.Type)
					throw new DynamicPropertyException("Value is different from dynamic property type!");

				textWriter.WriteString(property.Value.ToString());
			}
			textWriter.WriteEndElement(); // _valueNode
			textWriter.WriteEndElement(); // _dynamicPropertyNode
		}

		#endregion
	}
}
